package com.dj.mingkong.portal.biz.service.system.impl;

import com.alibaba.fastjson.JSON;
import com.dj.mingkong.common.constant.RedisConstants;
import com.dj.mingkong.common.enums.LoginLogAppCodeEnum;
import com.dj.mingkong.common.enums.LoginLogTypeEnum;
import com.dj.mingkong.common.enums.RoleEnum;
import com.dj.mingkong.common.enums.StatusEnum;
import com.dj.mingkong.common.exception.ValidateException;
import com.dj.mingkong.common.infrastructure.base.dto.context.UserInfoContext;
import com.dj.mingkong.common.infrastructure.eo.database.music.BoxEo;
import com.dj.mingkong.common.infrastructure.eo.database.system.*;
import com.dj.mingkong.common.infrastructure.eo.mongodb.system.UserMgEo;
import com.dj.mingkong.common.microservice.UserContext;
import com.dj.mingkong.common.util.*;
import com.dj.mingkong.portal.biz.constant.Constants;
import com.dj.mingkong.portal.biz.controller.system.vo.request.LoginVo;
import com.dj.mingkong.portal.biz.controller.system.vo.request.RegisterVo;
import com.dj.mingkong.portal.biz.controller.system.vo.request.UserUpdateVo;
import com.dj.mingkong.portal.biz.controller.system.vo.response.UserInfoVo;
import com.dj.mingkong.portal.biz.controller.system.vo.response.UserVo;
import com.dj.mingkong.portal.biz.core.base.context.UserContextHolder;
import com.dj.mingkong.portal.biz.enums.BoxTypeEnum;
import com.dj.mingkong.portal.biz.repository.database.music.BoxRepository;
import com.dj.mingkong.portal.biz.repository.database.system.*;
import com.dj.mingkong.portal.biz.repository.mongodb.system.UserMgRepository;
import com.dj.mingkong.portal.biz.service.system.UserService;
import com.dj.mingkong.portal.biz.util.JwtUtil;
import com.google.code.kaptcha.Producer;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 用户服务实现类
 *
 * @author zenglonghui
 * @date: 2022/8/18 10:20
 */
@Slf4j
@Service("userService")
public class UserServiceImpl implements UserService {
    @Resource
    private Producer verifyCodeProducer;
    @Resource
    private JwtUtil jwtUtil;
    @Resource
    private RedisTemplate<String, String> redisTemplate;
    @Resource
    private UserRepository userRepository;
    @Resource
    private RoleRepository roleRepository;
    @Resource
    private UserRoleRepository userRoleRepository;
    @Resource
    private UserContextHolder userContextHolder;
    @Resource
    private UserLoginLogRepository userLoginLogRepository;
    @Resource
    private BoxRepository boxRepository;
    @Resource
    private DictionaryRepository dictionaryRepository;
    @Resource
    private UserMgRepository userMgRepository;

    @Override
    public void getVerifyCodeImage(HttpServletRequest request, HttpServletResponse response) {
        try (ServletOutputStream out = response.getOutputStream()) {
            final String key = request.getParameter("key");
            response.setDateHeader("Expires", 0);
            response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
            response.addHeader("Cache-Control", "post-check=0, pre-check=0");
            response.setHeader("Pragma", "no-cache");
            response.setContentType("image/jpeg");
            //生成验证码
            final String capText = verifyCodeProducer.createText();
            log.info("生成验证码key:{}, capText:{}", key, capText);
            redisTemplate.boundValueOps(RedisConstants.VERIFY_CODE + key)
                    .set(capText, RedisConstants.VERIFY_CODE_EXPIRE_TIME, TimeUnit.MINUTES);
            //向客户端写出
            final BufferedImage image = verifyCodeProducer.createImage(capText);
            ImageIO.write(image, "jpg", out);
            out.flush();
        } catch (Exception e) {
            log.error("getVerifyCodeImage error", e);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void insert(RegisterVo registerVo, HttpServletRequest request) {
        checkVerifyCode(registerVo.getVerifyCode(), request);
        final UserEo userEo = buildUserEo(registerVo);
        userRepository.save(userEo);
        saveUserRole(userEo);
        saveBox(userEo);
        saveUserMg(userEo);
    }

    private void saveUserMg(UserEo userEo) {
        final UserMgEo userMgEo = SmartBeanCopy.copy(userEo, UserMgEo.class);
        userMgRepository.save(userMgEo);
    }

    private void saveBox(UserEo userEo) {
        final BoxEo boxEo = BoxEo.builder().name(Constants.DEFAULT_BOX_NAME)
                .description(Constants.DEFAULT_BOX_NAME).userId(userEo.getId())
                .type(BoxTypeEnum.DEFAULT.getType()).build();
        boxRepository.save(boxEo);
    }

    @Override
    public String login(LoginVo loginVo, HttpServletRequest request) {
        //校验验证码
        if (isCheckCode(request)) {
            checkVerifyCode(loginVo.getVerifyCode(), request);
        }
        var userEo = userRepository.oneByUsername(loginVo.getUsername());
        //校验账号密码
        checkUser(userEo, loginVo);
        //生成token
        final String token = createToken(userEo);
        redisTemplate.boundValueOps(RedisConstants.TOKEN + userEo.getUsername())
                .set(token, RedisConstants.EXPIRE_TIME, TimeUnit.MINUTES);
        log.info("开启线程登录记录----------------->");
        ThreadPoolUtils.getThreadPool().execute(() -> saveLoginLog(request, userEo));
        return token;
    }

    private String buildAvatarUrl(String avatarUrl) {
        if (StringUtils.isBlank(avatarUrl)) {
            final Optional<DictionaryEo> eoOptional = dictionaryRepository.oneOptByFullKey(Constants.DEFAULT_USER_AVATAR_DICT_FULL_KEY);
            return eoOptional.isEmpty() ? "" : eoOptional.get().getDictValue();
        }
        return avatarUrl;
    }

    @Override
    public void logout() {
        userContextHolder.getCurrentUserOpt().ifPresent(currentUser -> {
            final Boolean delete = redisTemplate.delete(RedisConstants.TOKEN + currentUser.getUsername());
            if (Boolean.FALSE.equals(delete)) {
                throw new ValidateException("logout error");
            }
            log.info("注销成功 username：{}", currentUser.getUsername());
        });
    }

    @Override
    public UserInfoVo info(Long userId) {
        final UserEo userEo = userRepository.getById(userId);
        AssertUtils.isNotNull(userEo, "userEo is null");
        final UserInfoVo userInfoVo = SmartBeanCopy.copy(userEo, UserInfoVo.class);
        userInfoVo.setRoles(getUserRoles(userId));
        userInfoVo.setAvatarUrl(buildAvatarUrl(userInfoVo.getAvatarUrl()));
        return userInfoVo;
    }

    @Override
    public UserVo getInfo(Long userId) {
        final UserEo userEo = userRepository.getById(userId);
        AssertUtils.isNotNull(userEo, "userEo invalid");
        return SmartBeanCopy.copy(userEo, UserVo.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateById(UserUpdateVo updateVo) {
        if (!UserContext.getUserId().equals(updateVo.getId())) {
            throw new ValidateException("Modify without permission");
        }
        final UserEo userEo = userRepository.getById(updateVo.getId());
        AssertUtils.isNotNull(userEo, "id invalid");
        final UserEo eo = SmartBeanCopy.copy(updateVo, UserEo.class);
        userRepository.updateById(eo);
    }

    private void saveLoginLog(HttpServletRequest request, UserEo userEo) {
        final UserLoginLogEo userLoginLogEo = UserLoginLogEo.builder()
                .loginType(LoginLogTypeEnum.PORTAL.getType()).userId(userEo.getId())
                .appCode(LoginLogAppCodeEnum.PC.getCode()).username(userEo.getUsername())
                .loginTime(LocalDateTime.now()).loginIp(IpUtils.getIpAddress(request)).build();
        userLoginLogRepository.save(userLoginLogEo);
    }

    private boolean isCheckCode(HttpServletRequest request) {
        final String ip = request.getRemoteAddr();
        final String url = request.getRequestURL().toString();
        final String key = RedisConstants.LOGIN_LIMIT.concat(url).concat(ip);
        final Long count = redisTemplate.opsForValue().increment(key, 1);
        if (Objects.isNull(count) || count.equals(1L)) {
            redisTemplate.expire(key, 1, TimeUnit.MINUTES);
        }
        if (Objects.nonNull(count) && count > Constants.LOGIN_COUNT) {
            log.info("用户IP[{}]访问地址[{}]超过了限定的次数{}", ip, url, Constants.LOGIN_COUNT);
            return true;
        }
        return false;
    }

    private String createToken(UserEo userEo) {
        final UserInfoContext userInfoContext = SmartBeanCopy.copy(userEo, UserInfoContext.class);
        final String userString = JSON.toJSONString(userInfoContext);
        log.info("把用户信息转成字符串:{}", userString);
        //获取角色
        final List<String> roleList = getUserRoles(userEo.getId());
        final String roles = JSON.toJSONString(roleList);
        log.info("把角色集合对象转成json:{}", roles);
        //登录成功,签发token   参数1:用户登陆名  参数2:用户实体类  参数3:角色
        return jwtUtil.createJwt(userInfoContext.getUsername(), userString, roles);
    }

    private List<String> getUserRoles(Long userId) {
        final List<UserRoleEo> userRoleEos = userRoleRepository.listByUserId(userId);
        if (userRoleEos.isEmpty()) {
            return Collections.emptyList();
        }
        final List<Long> roleIds = userRoleEos.stream().map(UserRoleEo::getRoleId).toList();
        final List<RoleEo> roleEos = roleRepository.listByIds(roleIds);
        return roleEos.stream().filter(roleEo -> StatusEnum.VALID.equalsIntVal(roleEo.getStatus()))
                .map(RoleEo::getCode).toList();
    }

    private void checkUser(UserEo userEo, LoginVo loginVo) {
        AssertUtils.isNotNull(userEo, "账号不存在！");
        if (StatusEnum.INVALID.equalsIntVal(userEo.getStatus())) {
            throw new ValidateException("账号已冻结，请联系管理员！");
        }
        if (!loginVo.getPassword().equals(AESUtils.decrypt(userEo.getPassword(), ""))) {
            throw new ValidateException("账号或密码错误！");
        }
    }

    private void saveUserRole(UserEo userEo) {
        final RoleEo roleEo = roleRepository.oneByCode(RoleEnum.USER.getCode());
        AssertUtils.isNotNull(roleEo, "user role is null");
        final UserRoleEo userRoleEo = UserRoleEo.builder().roleId(roleEo.getId())
                .userId(userEo.getId()).build();
        userRoleRepository.save(userRoleEo);
    }

    private UserEo buildUserEo(RegisterVo registerVo) {
        if (userRepository.existByUsername(registerVo.getUsername())) {
            throw new ValidateException("该账号已被注册");
        }
        final UserEo userEo = SmartBeanCopy.copy(registerVo, UserEo.class);
        userEo.setPassword(AESUtils.encrypt(registerVo.getPassword(), ""))
                .setNickname(registerVo.getUsername()).setMobile(registerVo.getUsername())
                .setRealName(Constants.UNKNOWN).setBirthday(LocalDate.now())
                .setInterest(Constants.UNKNOWN).setPersonality(Constants.UNKNOWN).setCity(Constants.UNKNOWN)
                .setIntroduction(Constants.UNKNOWN);
        return userEo;
    }

    private void checkVerifyCode(String verifyCode, HttpServletRequest request) {
        final String key = request.getHeader(Constants.VERIFY_CODE_KEY);
        log.info("新用户注册检验验证码key:{}", key);
        final String code = redisTemplate.boundValueOps(RedisConstants.VERIFY_CODE + key).get();
        log.info("新用户注册检验验证码 用户输入:{}, redis:{}", verifyCode, code);
        if (StringUtils.isBlank(code) || !verifyCode.equalsIgnoreCase(code)) {
            throw new ValidateException("验证码错误");
        }
        redisTemplate.delete(RedisConstants.VERIFY_CODE + key);
        log.info("新用户注册检验验证码通过，删除本地验证码成功");
    }
}
